var utils = require('./utils/utils');
var isBrowser = typeof document !== "undefined";

class Fly {
  constructor(engine) {
    this.engine = engine || XMLHttpRequest;

    this.default = this   //For typeScript

    /**
     * Add  lock/unlock API for interceptor.
     *
     * Once an request/response interceptor is locked, the incoming request/response
     * will be added to a queue before they enter the interceptor, they will not be
     * continued  until the interceptor is unlocked.
     *
     * @param [interceptor] either is interceptors.request or interceptors.response
     */
    function wrap(interceptor) {
      var resolve;
      var reject;

      function _clear() {
        interceptor.p = resolve = reject = null;
      }

      utils.merge(interceptor, {
        lock() {
          if (!resolve) {
            interceptor.p = new Promise((_resolve, _reject) => {
              resolve = _resolve
              reject = _reject;
            })
          }
        },
        unlock() {
          if (resolve) {
            resolve()
            _clear();
          }
        },
        clear() {
          if (reject) {
            reject("cancel");
            _clear();
          }
        }
      })
    }

    var interceptors = this.interceptors = {
      response: {
        use(handler, onerror) {
          this.handler = handler;
          this.onerror = onerror;
        }
      },
      request: {
        use(handler) {
          this.handler = handler;
        }
      }
    }

    var irq = interceptors.request;
    var irp = interceptors.response;
    wrap(irp);
    wrap(irq);

    this.config = {
      method: "GET",
      baseURL: "",
      headers: {},
      timeout: 0,
      params: {}, // Default Url params
      parseJson: true, // Convert response data to JSON object automatically.
      withCredentials: false
    }
  }

  request(url, data, options) {
    var engine = new this.engine;
    var contentType = "Content-Type";
    var contentTypeLowerCase = contentType.toLowerCase();
    var interceptors = this.interceptors;
    var requestInterceptor = interceptors.request;
    var responseInterceptor = interceptors.response;
    var requestInterceptorHandler = requestInterceptor.handler;
    var promise = new Promise((resolve, reject) => {
      if (utils.isObject(url)) {
        options = url;
        url = options.url;
      }
      options = options || {};
      options.headers = options.headers || {};

      function isPromise(p) {
        // some  polyfill implementation of Promise may be not standard,
        // so, we test by duck-typing
        return p && p.then && p.catch
      }

      /**
       * If the request/response interceptor has been locked，
       * the new request/response will enter a queue. otherwise, it will be performed directly.
       * @param [promise] if the promise exist, means the interceptor is  locked.
       * @param [callback]
       */
      function enqueueIfLocked(promise, callback) {
        if (promise) {
          promise.then(() => {
            callback()
          })
        } else {
          callback()
        }
      }

      // make the http request
      function makeRequest(options) {
        data = options.body;
        // Normalize the request url
        url = utils.trim(options.url);
        var baseUrl = utils.trim(options.baseURL || "");
        if (!url && isBrowser && !baseUrl) url = location.href;
        if (url.indexOf("http") !== 0) {
          var isAbsolute = url[0] === "/";
          if (!baseUrl && isBrowser) {
            var arr = location.pathname.split("/");
            arr.pop();
            baseUrl = location.protocol + "//" + location.host + (isAbsolute ? "" : arr.join("/"))
          }
          if (baseUrl[baseUrl.length - 1] !== "/") {
            baseUrl += "/"
          }
          url = baseUrl + (isAbsolute ? url.substr(1) : url)
          if (isBrowser) {

            // Normalize the url which contains the ".." or ".", such as
            // "http://xx.com/aa/bb/../../xx" to "http://xx.com/xx" .
            var t = document.createElement("a");
            t.href = url;
            url = t.href;
          }
        }

        var responseType = utils.trim(options.responseType || "")
        var isGet = options.method === "GET";
        var dataType = utils.type(data);
        var params = options.params || {};

        // merge url params when the method is "GET" (data is object)
        if (isGet && dataType === "object") {
          params = utils.merge(data, params)
        }
        // encode params to String
        params = utils.formatParams(params);

        // save url params
        var _params = [];
        if (params) {
          _params.push(params);
        }
        // Add data to url params when the method is "GET" (data is String)
        if (isGet && data && dataType === "string") {
          _params.push(data);
        }

        // make the final url
        if (_params.length > 0) {
          url += (url.indexOf("?") === -1 ? "?" : "&") + _params.join("&");
        }

        engine.open(options.method, url);

        // try catch for ie >=9
        try {
          engine.withCredentials = !!options.withCredentials;
          engine.timeout = options.timeout || 0;
          if (responseType !== "stream") {
            engine.responseType = responseType
          }
        } catch (e) {
        }

        var customContentType = options.headers[contentType] || options.headers[contentTypeLowerCase];

        // default content type
        var _contentType = "application/x-www-form-urlencoded";
        // If the request data is json object, transforming it  to json string,
        // and set request content-type to "json". In browser,  the data will
        // be sent as RequestBody instead of FormData
        if (utils.trim((customContentType || "").toLowerCase()) === _contentType) {
          data = utils.formatParams(data);
        } else if (!utils.isFormData(data) && ["object", "array"].indexOf(utils.type(data)) !== -1) {
          _contentType = 'application/json;charset=utf-8'
          data = JSON.stringify(data);
        }
        //If user doesn't set content-type, set default.
        if (!(customContentType || isGet)) {
          options.headers[contentType] = _contentType;
        }

        for (var k in options.headers) {
          if (k === contentType && utils.isFormData(data)) {
            // Delete the content-type, Let the browser set it
            delete options.headers[k];
          } else {
            try {
              // In browser environment, some header fields are readonly,
              // write will cause the exception .
              engine.setRequestHeader(k, options.headers[k])
            } catch (e) {
            }
          }
        }

        function onresult(handler, data, type) {
          enqueueIfLocked(responseInterceptor.p, function () {
            if (handler) {
              //如果失败，添加请求信息
              if (type) {
                data.request = options;
              }
              var ret = handler.call(responseInterceptor, data, Promise)
              data = ret === undefined ? data : ret;
            }
            if (!isPromise(data)) {
              data = Promise[type === 0 ? "resolve" : "reject"](data)
            }
            data.then(d => {
              resolve(d)
            }).catch((e) => {
              reject(e)
            })
          })
        }


        function onerror(e) {
          e.engine = engine;
          onresult(responseInterceptor.onerror, e, -1)
        }

        function Err(msg, status) {
          this.message = msg
          this.status = status;
        }

        engine.onload = () => {
          // The xhr of IE9 has not response field
          var response = engine.response || engine.responseText;
          if (response && options.parseJson && (engine.getResponseHeader(contentType) || "").indexOf("json") !== -1
            // Some third engine implementation may transform the response text to json object automatically,
            // so we should test the type of response before transforming it
            && !utils.isObject(response)) {
            response = JSON.parse(response);
          }

          var headers = engine.responseHeaders;
          // In browser
          if (!headers) {
            headers = {};
            var items = (engine.getAllResponseHeaders() || "").split("\r\n");
            items.pop();
            items.forEach((e) => {
              if (!e) return;
              var key = e.split(":")[0]
              headers[key] = engine.getResponseHeader(key)
            })
          }
          var status = engine.status
          var statusText = engine.statusText
          var data = {data: response, headers, status, statusText};
          // The _response filed of engine is set in  adapter which be called in engine-wrapper.js
          utils.merge(data, engine._response)
          if ((status >= 200 && status < 300) || status === 304) {
            data.engine = engine;
            data.request = options;
            onresult(responseInterceptor.handler, data, 0)
          } else {
            var e = new Err(statusText, status);
            e.response = data
            onerror(e)
          }
        }

        engine.onerror = (e) => {
          onerror(new Err(e.msg || "Network Error", 0))
        }

        engine.ontimeout = () => {
          onerror(new Err(`timeout [ ${engine.timeout}ms ]`, 1))
        }
        engine._options = options;
        setTimeout(() => {
          engine.send(isGet ? null : data)
        }, 0)
      }

      enqueueIfLocked(requestInterceptor.p, () => {
        utils.merge(options, JSON.parse(JSON.stringify(this.config)));
        var headers = options.headers;
        headers[contentType] = headers[contentType] || headers[contentTypeLowerCase] || "";
        delete headers[contentTypeLowerCase]
        options.body = data || options.body;
        url = utils.trim(url || "");
        options.method = options.method.toUpperCase();
        options.url = url;
        var ret = options;
        if (requestInterceptorHandler) {
          ret = requestInterceptorHandler.call(requestInterceptor, options, Promise) || options;
        }
        if (!isPromise(ret)) {
          ret = Promise.resolve(ret)
        }
        ret.then((d) => {
          //if options continue
          if (d === options) {
            makeRequest(d)
          } else {
            resolve(d)
          }
        }, (err) => {
          reject(err)
        })
      })
    })
    promise.engine = engine;
    return promise;
  }

  all(promises) {
    return Promise.all(promises)
  }

  spread(callback) {
    return function (arr) {
      return callback.apply(null, arr);
    }
  }
}

//For typeScript
Fly.default = Fly;

["get", "post", "put", "patch", "head", "delete"].forEach(e => {
  Fly.prototype[e] = function (url, data, option) {
    return this.request(url, data, utils.merge({method: e}, option))
  }
});
["lock", "unlock", "clear"].forEach(e => {
  Fly.prototype[e] = function () {
    this.interceptors.request[e]();
  }
});

module.exports = Fly;


